<?php
/**
 * @package db
 */

require_once '_logger.php';
require_once '_policy.php';
require_once '_exceptions.php';
require_once '_common.php';


/**
 * Abstract class which provide transport of an row from the databese
 * to a object in the memory and backwards.
 */
abstract class DBObject {
	const TABLE_CODESLONG = "codeslong";
	const TABLE_OFFERS = "offers";
	const TABLE_USERS = "users";
	const TABLE_SETTINGS = "settings";

	/**
	 * Function that allow any descendant to fetch instances of itself from the mysql query.
	 *
	 * @param resource $result
	 * @return DBObject
	 */
	public abstract static function mysqlFetch(resource $resource);
	
	/**
	 * Escape $value with database's escape function.
	 * @see mysql_real_escape_string
	 *
	 * @param mixed $value
	 * @return string - escaped value
	 */
	 protected function processValueBeforeInsert($value) {
		if (is_null($value)) {
			return "NULL";
		} else {
			return "'".mysql_real_escape_string($value, DBConnectorMySQL::getInitialResource())."'";
		}
	}
	
	/**
	 * Put members from $array into new instace of $class. In case that members of array matches
	 * template for a Codemanager 
	 * ({Constants::DB_COMMON_CODESPREFIXFROMAFORM}{SiteLang::$LANG_AVAILABLE ->
	 * 		-> one of possible languages shortcuts}_{value from $possibleCodes})
	 * e.g. c_en_texts
	 * .
	 * @param array $array - an array to process (Frequently used is $_REQUEST)
	 * @param array $possibleCodes - which members of $array should be processed as a localizable text
	 */
	public function processValuesFromArrayWithCodes($array = null, $possibleCodes = null) {
		$info = SiteContext::getSiteContext();
		$logger = Logger::getLogger();
		$cm = CodeManager::getCodeManager();
		$codes = array();
		if (is_null($array)) {
			$array = $_REQUEST; 
		}
		
		foreach ($array as $name => $value) {
			if (property_exists($this, $name)) {
				$this->$name = $value;
				$logger->debug("object's new property $name is: ".$this->$name);
			} elseif (preg_match("/^".Constants::DB_COMMON_CODESPREFIXFROMAFORM."/", $name)) {
				$strippedName = preg_replace("/^".Constants::DB_COMMON_CODESPREFIXFROMAFORM."/", "", $name);
				// processing codes
				foreach (SiteLang::$LANG_AVAILABLE as $availableLang => $subAvailableLang) {
					if (preg_match("/^".$availableLang."_/", $strippedName)) {
						$variable = preg_replace("/^".$availableLang."_/", "", $strippedName);
						foreach ($possibleCodes as $nameCode => $valueCode) {
							if (preg_match("/$valueCode$/", $variable)) {
								$codes[$valueCode][$availableLang] = $value;
							}
						}
					}
				}
			} 
		}
		foreach($codes as $codeVariableName => $code) {
			// Get size of codes.
			$maxCurrSize = 0;
			foreach ($code as $codeName => $codeValue) {
				$size = strlen($codeValue);
				if ($size > $maxCurrSize) {
					$maxCurrSize = $size;
				}
			}
			if ($maxCurrSize == 0) {
				// nothing was filled
			} elseif ($maxCurrSize < 0) {
				throw new ExceptionBadCode("Bad codes");
			} elseif ($maxCurrSize < CodesLong::maxAllowedSize()) {
				$codeObject = new CodesLong();
				$codeObject->processValuesFromArrayWithCodes($code);
				if (@is_string($_REQUEST[$codeVariableName]) && $_REQUEST[$codeVariableName] != "") {
					$codeObject->id = $_REQUEST[$codeVariableName];
					$idOfCode = $codeObject->update();
				} else {
					$idOfCode = $codeObject->insert();
				}
				if (is_null($idOfCode)) {
					throw new Exception("Unexpected error.");
				}
				/** add this code into calling class */
				$this->processValuesFromArrayWithCodes(array($codeVariableName => $idOfCode));
			} else {
				Error::addErrorMessage($cm->get("30215"));
				throw new ExceptionBadCode("Too much long code.");
			}
		}
	}
	
	/**
	 * Process images from $array and delete them from the filesystem
	 * @param array $array - set of images to delete
	 * @param string $fullDir - where are images stored
	 */
	public function processImagesToDelete($array = null, string $fullDir) {
		if (is_null($array)) {
			$array = $_POST; 
		}
		if (file_exists($fullDir)) {
			$images = array();
			foreach($array as $member => $value) {
				if (preg_match("/img_/", $member)) {
					$images[] = preg_replace("/img_/", "", $member);
				}
			}
			$handle = opendir($fullDir);
			function imageMatchesFile($images, $file) {
				foreach($images as $image) {
					$fileEscaped = preg_replace("/[\. ]/", "_", $file);
					if (preg_match("/$image/", $fileEscaped)) {
						return true;
					}
				}
				return false;
			}
			while (false !== ($file = readdir($handle))) {
		        if (Policy::possibleImgExt($file) && $image = imageMatchesFile($images, preg_replace("/(.*)\.[^\.]+$/", "\\1", $file))) {
		        	unlink($fullDir.Constants::FS_DIRDELIMITER.$file);
		        }
		    }
		}
	}
	
	/**
	 * Count of maximum tries to generate new filename of a picture.
	 * @see self::processImagesFromArrayAndSaveThem
	 */
	const MAXTRIES_GENPICTUREFILENAME = 10;

	/**
	 * Generate new filename based on current unix timestamp and extension of an original file.
	 * @param string $fullDir
	 * @param string $originalName
	 */
	private static function generateFileName(string $fullDir, string $originalName) {
		$newFileName = time();
		$newFileExt = preg_replace("/.*(\.[^.]+)$/", "\\1", $originalName);
		return $fullDir.Constants::FS_DIRDELIMITER.$newFileName.$newFileExt;
	}
	
	/**
	 * Save and process images from $array and save them into $fullDir
	 * @param array $array - array of images
	 * @param string $fullDir - directory to save images in
	 * @param bool $addWatermark - add or not watermark
	 */
	public function processImagesFromArrayAndSaveThem($array = null, string $fullDir, $addWatermark = false) {
		$info = SiteContext::getSiteContext();
		$logger = Logger::getLogger();
		$cm = CodeManager::getCodeManager();
		$error = Error::getError();
		$codes = array();
		if (is_null($array)) {
			$array = $_FILES; 
		}
		
		$isError = false;
		
		if (!file_exists($fullDir)) {
			if (!mkdir($fullDir, 0770, true)) {
				Logger::getLogger()->error("Cannot create directory '$fullDir'.");
				throw new ExceptionFileUploadFailed("Failed uploading of all images - cannot create directory.");
			}
		}
		
		foreach ($array as $file) {
			if ($file['name'] == "" || $file['tmp_name'] == "" || $file['type'] == "") {
				continue;
			} 
			if (!Policy::possibleImgType($file['type'])) {
				$error->addErrorMessage($cm->get("30213", array($file['name'])));
				$isError = true;
				continue;
			}
			if ($file['error'] > 0) {
				$error->addErrorMessage($cm->get("30214", array($file['name'])));
				$isError = true;
				continue;
			}
			
			$fileName = "";
			for ($countTryes = 0; $countTryes < self::MAXTRIES_GENPICTUREFILENAME; ++$countTryes) {
				if (!($fileName = self::generateFileName($fullDir, $file['name']))) {
					continue;
				}
				if (!touch($fileName)) {
					continue;
				}
				if (filesize($fileName) == 0) {
					break;
				}
			}
			if ($countTryes == self::MAXTRIES_GENPICTUREFILENAME) {
				$error->addErrorMessage($cm->get("30214", array($file['name'])));
				$isError = true;
				continue;
			}
			
			if (!move_uploaded_file($file['tmp_name'], $fileName)) {
				$error->addErrorMessage($cm->get("30214", array($file['name'])));
				$isError = true;
				continue;
			}
			if ($addWatermark) {
				$img = new Img($fileName);
				$img->addWatermark();
			}
		}
		if ($isError) {
			throw new ExceptionFileUploadFailed();
		}
	}
	
	/**
	 * Descendants must implement this function.
	 * This is used when looking for a table name which
	 * instances of this class belongs in.
	 * 
	 * @return string - name of sql table
	 */
	protected abstract function tableName();
	
	/**
	 * Insert object in the database.
	 * @see self::tableName();
	 */
	public function insert() {
		$connector = SiteContext::getSiteContext()->getDBConnector();
		$logger = Logger::getLogger();
		
		$strBegin = "INSERT INTO `".$this->tableName()."` (";
		$strEnd = "VALUES (";
		
		foreach ($this as $var => $value) {
			$strBegin .= "`$var`, ";
			$strEnd .= $this->processValueBeforeInsert($value) . ", ";
		}
		$strBegin = preg_replace("/, $/", ") ", $strBegin);
		$strEnd = preg_replace("/, $/", ") ", $strEnd);
		
		$res = $connector->query($strBegin.$strEnd);
		
		if (property_exists($this, "id")) {
			$this->id = mysql_insert_id(DBConnectorMySQL::getInitialResource());
			return $this->id;
		} else {
			return mysql_insert_id(DBConnectorMySQL::getInitialResource());
		}
	}
	
	/**
	 * Update object to the database.
	 */
	public function update() {
		$connector = SiteContext::getSiteContext()->getDBConnector();
		$logger = Logger::getLogger();
		
		$res = $connector->query("select * from ".$this->tableName()." where id = $this->id limit 1");
		
		// TODO change this to smth else
		if ($this instanceof CodesLong) {
			$oldObject = CodesLong::mysqlFetch($res);
		} elseif ($this instanceof Offer) {
			$oldObject = Offer::mysqlFetch($res);
		} elseif ($this instanceof User) {
			$oldObject = User::mysqlFetch($res);
		} elseif ($this instanceof Settings) {
			$oldObject = Settings::mysqlFetch($res);
		} else {
			$oldObject = self::mysqlFetch($res); // TODO use PHP 5.3
		}
		if (!$oldObject) {
			$logger->error("Cannot update $this->id in ".$this->tableName());
			throw new ExceptionDisplayable("Update failed. Please Contact administrator");
		}
		
		$str = "update `".$this->tableName()."` set ";
		$changed = false;
		
		foreach ($this as $var => $value) {
			if ($oldObject->$var != $value && $value !== null) {
				$str .= "`$var` = " .$this->processValueBeforeInsert($value) . ", ";
				$changed = true;
			}
		}
		if ($changed) {
			$str = preg_replace("/, $/", " where ".$this->tableName().".`id` = $this->id limit 1", $str);
			$res = $connector->query($str);
		}
		return $this->id;
	}
}

/**
 * Instances of this class are images of
 * rows in the table codeslong
 *
 */
class CodesLong extends DBObject {
	public $id;
	public $shortCut;
	public $cs;
	public $en;
	public $de;
	
	/**
	 * Fetch instance of CodesLong class from the database.
	 * @return CodesLong
	 */
	static function mysqlFetch(resource $resource) {
		return mysql_fetch_object($resource, __CLASS__);
	}
	
	/**
	 * Maximum allowed size for a code.
	 */
	static function maxAllowedSize() {
		// TODO get it from the database
		return 4096;
	}
	
	/**
	 * Insert code into the database.
	 * @return int - Id of inserted code.
	 * @throws ExceptionDBInsert in case of failure.
	 */
	public function insertOld() {
		$connector = SiteContext::getSiteContext()->getDBConnector();
		$logger = Logger::getLogger();
		
		$str = "INSERT INTO `".self::TABLE_CODESLONG."` (`id` ,`shortCut` ,`cs` ,`de`, `en`)
		VALUES (NULL , 
		".$this->processValueBeforeInsert($this->shortCut).",
		".$this->processValueBeforeInsert($this->cs).",
		".$this->processValueBeforeInsert($this->de).", 
		".$this->processValueBeforeInsert($this->en).");";
		
		$res = $connector->query($str);
		return mysql_insert_id(DBConnectorMySQL::getInitialResource());
	}
	
	/** @see DBObject::tableName */
	protected function tableName() {
		return self::TABLE_CODESLONG;
	}
	/**
	 * Overriden function insert.
	 * Let the database decide which id to use.
	 */
	public function insert() {
		$this->id = null;
		return parent::insert();
	}
}

/**
 * Instances of this class can easily use translated texts
 * which are stored in CodesLong (codeslong database)
 */
abstract class MultiLangDBObject extends DBObject {
	/**
	 * Parses code shortcut from datatabases and resolves its content.
	 *
	 * @param string $codeId
	 * @return string
	 */
	public function resolveCodeShortcut(string $codeId) {
		$result = NULL;
		$table = NULL;
		$obj = NULL;
		$id = NULL;
		$info = SiteContext::getSiteContext();
		if ($codeId == null) {
			return "";
		}
		
		preg_match('/([0-9]*)$/i', $codeId, $matches);
		$id = (int)$matches[1];
		
		if (!is_int($id)) {
			throw new ExceptionBadCode("Bad id '$id' for '$codeId'. Type was: " . gettype($id));
		}
		$table = self::TABLE_CODESLONG;
		$function = "CodesLong::mysqlFetch";
			
		$res = SiteContext::getSiteContext()->getDBConnector()->query("select * from $table where id = $id limit 1");
		if (!is_object($obj = call_user_func($function, $res))) {
			throw new Exception("Cannot find appopriate code for $codeId");
		}
		eval('$result = $obj->' . $info->getSiteLang()->getLang() . ';');
		Logger::getLogger()->debug("message is: " . $result);
		
		return $result;
	}
}

class Settings extends MultiLangDBObject {
	public $mainPage;
	public $contactPage;
	public $homePageUrl;
	public $eurInCzk;
	public $logLevel;
	public $logFile;
	public $watermark;
	public $aboutPage;
	
	/**
	 * instance of this object which reprezent on row in settings table
	 */
	private static $settings;
	
	/**
	 * @see DBObject::tableName
	 */
	protected function tableName() {
		return self::TABLE_SETTINGS;
	}
	
	public function __construct() {
		
	}
	
	/**
	 * @see DBObject::mysqlFetch
	 * @deprecated use of this method is deprecated
	 * @return Settings
	 */
	static function mysqlFetch(resource $resource) {
		return mysql_fetch_object($resource, __CLASS__);
	}
	
	/** Columns which are localized. Used when filling form. */
	public static $CODES_COLUMNS = array("mainPage", "contactPage", "aboutPage");
	
	/**
	 * A way how to receive settings from the database.
	 * @return Settings
	 */
	public static function getSettings() {
		if (!self::$settings instanceof Settings) {
			$res = $connector = SiteContext::getSiteContext()->getDBConnector()->query("select * from settings limit 1");
			self::$settings = self::mysqlFetch($res);
		}
		return self::$settings;
	}
	
	
	/**
	 * Overriden
	 * Sets some important members the default value
	 * @see DBObject::processValuesFromArrayWithCodes
	 */
	public function processValuesFromArrayWithCodes($array = null, $possibleCodes = null) {
		parent::processValuesFromArrayWithCodes($array, $possibleCodes);
		// Neccessary checks and sets.
		$this->id = null;
	}
}

/**
 * Instances of this class can easily access pictures on the filesystem
 * @see Offer
 * @see User
 */
abstract class MultiLangFSDBObject extends MultiLangDBObject {
	/** @var resource - openned directory */
	private $resImgDir;
	
	/**
	 * Returns subdir's name where object's images are stored. Assuming all pictures are stored in one root
	 * directory (e.g. img .. so images of an offer should be stored in img/{imgSubdir()}).
	 * 
	 * @return string
	 */
	abstract public function imgSubdir();
	
	/**
	 * While path to the directory whera are images stored.
	 * @return string - path
	 */
	public function imgDir() {
		return SiteContext::getSiteContext()->getSiteInf()->getPathFilesystemImg() . Constants::FS_DIRDELIMITER . $this->imgSubdir();
	}
	
	/**
	 * Get directory resource, where images for this object are stored.
	 * 
	 * @return resource
	 */
	public function resourceImgDir() {
		if ($this->resImgDir == NULL) {
			$this->resImgDir = @opendir($this->imgDir());
		}
		return $this->resImgDir;
	}
	
	/**
	 * When this method is called in a cycle
	 * it returns filenames of images which belongs to this object
	 * 
	 * @return string on success, false when done
	 */
	public function fetchImgFilename() {
		$dir = $this->resourceImgDir();
		while ($filename = @readdir($this->resourceImgDir())) {
			if (Policy::possibleImgExt($filename))
				return $filename;
			else
				Logger::getLogger()->debug("$filename is not valid picture");
		}
		return false;
	}
	
	/**
	 * In case that there is not already feasible picture, create new image from the $fileName.
	 * New image's name will be $newName and its greater size will be equal to $size.
	 * 
	 * @param string $fileName
	 * @param string $prefix
	 * @param string $newName
	 * @param int $size
	 * 
	 * @return string - In case, that new image was created, new filename is returned. Otherwise old file name is returned.
	 */
	public function resizePictureIfThereIsNotAlreadyOne(string $fileName, string $prefix, string $newName, int $width) {
		$filePath = SiteContext::getSiteContext()->getSiteInf()->getPathFilesystemImg() . "/" . $this->imgSubdir();
		if (!file_exists("$filePath/$newName")) {
			/** It is neccessary to create a thumbnail. */
			$origImg = new Img("$filePath/$fileName");
			
			$ratio = Constants::IMG_RATIO;
			$maxWidth = $width;
			$maxHeight = $width * $ratio;
			
			$origWidth = $origImg->getWidth();
			$origHeight = $origImg->getHeight();
			
			if ($origHeight / $origWidth < $ratio) {
				// It is neccesary to adjust width first.
				$newWidth = $maxWidth;
				// And then count the new height proportionaly.
				$newHeight = $newWidth / $origWidth * $origHeight;
			} else {
				$newHeight = $maxHeight;
				$newWidth = $newHeight / $origHeight * $origWidth;
			}
			// Resize the image
			$newImg = $origImg->resize($newWidth, $newHeight, "$filePath/$newName");
			
			$fileName = $newName;
		}
		return $fileName;
	}
}

/**
 * Instances of this class are images of a one row from
 * the table offers.
 * So it actually stands for one offer.
 */
class Offer extends MultiLangFSDBObject {
	public $locality;
	public $town;
	public $street;
	public $zip;
	public $rentOrSale;
	public $price;
	public $currency;
	public $size;
	public $agentId;
	public $title;
	public $text;
	public $id;
	public $creationTime;
	public $top;
	public $status;
	public $seoUrl;
	public $seoDescription;
	public $seoKeywords;
	
	public function __get($name) {
		if (property_exists($this, $name)) {
			return $this->$name;
		}
        return null;
    }
    
	/**
	 * @see DBObject::tableName
	 */
	protected function tableName() {
		return self::TABLE_OFFERS;
	}
	
	/**
	 * @see DBObject::mysqlFetch
	 * @return Offer
	 */
	static function mysqlFetch(resource $resource) {
		return mysql_fetch_object($resource, __CLASS__);
	}
	
	/** Columns which are localized. Used when filling a form. */
	public static $CODES_COLUMNS = array("text", "title");
	
	/** 
	 * Similar values as in Offer::$CODES_COLUMNS
	 * but this is used in search
	 */
	public static $CODES_COLUMNS_ASSOC = array("text" => true, "title" => true);
	
	/**
	 * @see MultiLangFSDBObject::imgSubdir()
	 * 
	 * @return string
	 */
	public function imgSubdir() {
		return Constants::FS_OFFERS_DIR . Constants::FS_DIRDELIMITER . $this->id;
	}
	
	/**
	 * Insert code into the database.
	 * 
	 * @throws ExceptionDBInsert in case of failure.
	 * @deprecated // TODO remove this method
	 */
	public function insertOld() {
		$connector = SiteContext::getSiteContext()->getDBConnector();
		$logger = Logger::getLogger();
		
		$str = "INSERT INTO `".self::TABLE_OFFERS."` (`id` ,`creationTime` ,`locality` ,`town` ,
		`street` ,`zip` ,`rentOrSale` ,`price` ,`currency` ,`size` ,`agentId` ,`title` ,`text`)
		VALUES (NULL, NULL , 
		".$this->processValueBeforeInsert($this->locality).",
		".$this->processValueBeforeInsert($this->town).",
		".$this->processValueBeforeInsert($this->street).",
		".$this->processValueBeforeInsert((int)$this->zip).",
		".$this->processValueBeforeInsert($this->rentOrSale).",
		".$this->processValueBeforeInsert(intval($this->price)).",
		".$this->processValueBeforeInsert($this->currency).",
		".$this->processValueBeforeInsert((int)$this->size).",
		".$this->processValueBeforeInsert($this->agentId).",
		".$this->processValueBeforeInsert($this->title).",
		".$this->processValueBeforeInsert($this->text).");";
		
		$res = $connector->query($str);
		
		return mysql_insert_id(DBConnectorMySQL::getInitialResource());
	}
	
	/**
	 * This fixes problems with a value of a checkbox which
	 * is actually not sent when unchecked in a form.
	 */
	private function fixValueOfTop() {
		if (!is_int($this->top)) {
			if ($this->top == "on") {
				$this->top = 1;
			} else {
				$this->top = 0;
			}
		}
	}
	
	/**
	 * This fixes problems with a value of a checkbox which
	 * is actually not sent when unchecked in a form.
	 */
	private function fixValueOfStatus() {
		if (!is_int($this->status)) {
			if ($this->status == "on") {
				$this->status = "hidden";
			} else {
				$this->status = "visible";
			}
		}
	}
	
	/**
	 * @see DBObject::insert
	 */
	public function insert() {
		$this->id = null;
		$this->creationTime = null;
		$this->zip = (int)$this->zip;
		$this->price = (int)$this->price;
		$this->size = (int)$this->size;
		
		$this->fixValueOfTop();
		$this->fixValueOfStatus();
		
		$this->id = parent::insert();
		return $this->id;
	}
	
	/**
	 * @see DBObject::update
	 */
	public function update() {
		$this->fixValueOfTop();
		$this->fixValueOfStatus();
		$this->id = parent::update();
		return $this->id;
	}
	
	/**
	 * Overriden
	 * Sets some important members the default value
	 * @see DBObject::processValuesFromArrayWithCodes
	 */
	public function processValuesFromArrayWithCodes($array = null, $possibleCodes = null) {
		parent::processValuesFromArrayWithCodes($array, $possibleCodes);
		// Neccessary checks and sets.
		$this->creationTime = null;
		$this->id = null;
		
		if (is_null($array)) {
			$array = $_REQUEST; 
		}
		if (@$array == $_REQUEST && !isset($array['top'])) {
			$this->top = 0;
		}
		if (@$array == $_REQUEST && !isset($array['status'])) {
			$this->status = "visible";
		}
	}
}

/**
 * Instances of this class has an image in a table users.
 */
class User extends MultiLangFSDBObject {
	public $id;
	public $firstName;
	public $surName;
	public $passwdHash;
	public $group;
	public $login;
	public $mail;
	public $phone;
	public $creationTime;
	
	/**
	 * @see DBObject::mysqlFetch
	 * @return User
	 */
	static function mysqlFetch(resource $resource) {
		return mysql_fetch_object($resource, __CLASS__);
	}
	
	/**
	 * @see MultiLangFSDBObject::imgSubdir
	 * @return string
	 */
	public function imgSubdir() {
		return Constants::FS_USERS_DIR . Constants::FS_DIRDELIMITER . (int)$this->id;
	}
	
	/**
	 * Full name of the user.
	 * @return string
	 */
	public function fullName() {
		return $this->firstName . " " . $this->surName;
	}
	
	/**
	 * Create hash for a password.
	 * @see Common::createHash
	 * @param string $inText - input text
	 * @param $saltHash - salt
	 * @param $mode - @see http://www.php.net/manual/en/function.hash.php - algo
	 */
	public static function createHash(string $inText, $saltHash = null, $mode = 'sha1'){ 
       return Common::createHash($inText, $saltHash, $mode);
    }
	
    /**
     * @see DBObject::tableName
     */
	protected function tableName() {
		return self::TABLE_USERS;
	}
	
	/**
     * @see DBObject::insert
     */
	public function insert() {
		$this->id = null;
		
		return parent::insert();
	}
}
?>